iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
JavaScript

Don't make JavaScript Just Surpise系列 第 13

運算子(Operator) 上篇

  • 分享至 

  • xImage
  •  

** 前面的文章用詞同樣的字(Operator)多使用運算符這個詞,雖然都看得懂,但繁體中文的翻譯上好像傾向使用運算子,後面文章我會統一使用運算子代表 Operator。前面的文章會在後面回頭整理的時候逐步調整,兩者在我的文章裡指涉的是同一個對象。


運算子的重要程度和型別可以說是不相上下,運算子的定義是「使用在運算式中操作運算元的符號」。
運算元指的是運算式裡被操作的資料,而運算式則是「透過運算元和運算式計算後返回一個值的程式碼片段」,運算式可彼此嵌套,因為會返回值,返回的值可能被當作其他運算式的運算元。

let val = 1 + (2 * 3);

上面的這段裡有兩個運算式 1 + (2 * 3)2*3val 本身不是運算元也不是運算子,只是一個接下運算式返回結果的變數。

式子中的 1,2,32*3 返回的值 都是運算元,運算子則是 +*() 不算是運算子,通常會被歸類在語法結構(語法符號)中,但使用這些符號會影響運算的結果。

運算子種類

運算子有許多種類,在 JS 中,按 MDN 的分類來看,大致會分為以下幾種:

  • 賦值運算子

  • 比較運算子

  • 邏輯運算子

  • 算術運算子

  • 字串運算子

  • 位元運算子

  • 條件運算子(又稱三元運算子)

  • 逗點運算子

  • 一元運算子

  • 關係運算子

我們會逐一帶過各種類運算子具有的主要成員,以及相關使用範例、注意事項等。
JS 中存在一元,二元,三元運算子,這邊的「元」指的即是該運算子需要配合幾個運算元使用,一元就是只需一個運算元,二元需兩個,三元則是用上三個運算元。

賦值運算子

賦值運算子指的就是 =
= 是這個類別中的主要成員,其他多為配上其他運算子後的簡化寫法。
簡化的寫法有:+=-=*=/=%=**=(ES6),<<=>>=>>>=&=^=|=
另外還有三個 ES2021 推的也是簡化寫法 &&=||=??=:

這邊不會介紹簡化的寫法的各個意義,會各自在對應類別中被看到。
簡化寫法的統一規則就是:

a = a + b;
//簡化後
a += b;

當等號左邊的被賦值對象會被作為運算元時,就可以省略把等號左邊對象寫在右邊的寫法。
如果這樣寫的話,右邊的運算會先做完結果,才和左邊做對應處理。

let a = 5;
a *= 3 + 2;
console.log(a);//25

上面的例子中,輸出為 25,因為 3 + 2 會先被運算後才進行和左邊簡化後的結果相運算,即使 + 的優先度比 * 低,但這裡應該比較優先度時把 *= 當作一個符號,而不是純看 *
本系列文章稍後會提到運算子優先度的部分,剩餘內容留待該部分再談。

回到主要賦值運算子 = 本身,做的事情就是把右邊的運算式的結果存到左邊。
關於這邊作用的 LHS 機制和 RHS 機制,已經在第 9 天的文章裡討論過,點擊連結後搜尋標題 作用域的結構與尋找行為 可以找到相關敘述。

結合性(Associativity)

運算式中一旦有運算子,就會牽涉到是從左到右結算,或從右到左結算,這個稱作結合性。
在不考慮運算子優先級的情況下,同一個運算子的結算方向非左即右,稱作左結合性和右節合性。
大多數的運算子是左結合性的,意味著從左往右計算。
但像是 = 就是右結合性的,計算等於的時候是從右往做計算的。

let a, b;
a = b = 10;
console.log(a);//10
console.log(b);//10

上面的式子會先做 b = 10,接著,a = b,即是右結合。(透過結果可證明,若為左結合,a = b 應該會導致 a 變為 undefined 而不是 10)。

解構賦值

解構賦值是 ES 6 引入的功能,讓我們能更簡潔地將返回值存入對應變數。

function foo(){
    return {bar:10};
}
let a = foo();
let {bar} = foo();
console.log(a);//{bar: 10}
console.log(bar);//10

let b = [1,2,3];
let [one,two,three] = b;
console.log(one);
console.log(two);
console.log(three);

使用 {}[] 作為左方的語法,如果為 {},則需要鍵對應回傳的鍵名,否則在沒有拋出錯誤的情況下(如右方為 nullundefined 會因為解構失敗直接拋錯),多會拿到 undefined。如果為 [],則會直接對應索引的位置賦予結果,如上述例子的 two 因為索引值為 1,拿到返回陣列中同樣索引值為 1 的 2

let s = {"bar": 10};
let { notExist = 'default', ...restArgs } = s;
console.log(notExist);//default
console.log(restArgs);//{bar:10}

解構賦值時也能使用默認值,...Args 其餘參數等方法來給予預設值或接多出來的值。

善用解構賦值,能讓程式寫起來更優雅。
雖然能用於嵌套的情境,但為了可讀性一般並不建議這樣做,還有返回值過多的場景可能也是

比較運算子和邏輯運算子

比較運算子和邏輯運算子的共通點是通常意圖上都會希望把他們的回傳當成一個條件結果、布林值使用,比較運算子必定回傳布林值,但邏輯運算子不一定。
比較運算子包含 ==!====!==>>=<<=
邏輯運算子包含 &&||! 和 ES 2021 引入的 ??

比較運算子

比較運算子的意圖通常是進行值得比較,邏輯運算子則是將多個布林值依邏輯判斷,這兩個運算子時常會聯合在一起使用。
關於 ===== 的比較與值得轉換,已介紹於 Day5== 和 === 段落,!=!== 就是對應到相等比較和嚴格相等比較。

>< 的比較結果會和使用 == 一樣對類型不同的比較對象進行轉型,轉型規則也是一樣。
至於如何判斷大小,讓我們直接舉出其中比較特別的例子來幫助理解:

console.log('a' > 'b');//false,字串以字典序(Lexicographical order)排序,a 的編碼在 b 之前,故為 false,參照對象為 UTF-16 的編碼表(https://zh.wikipedia.org/zh-tw/UTF-16)
console.log('ab' > 'b');//true,當字首開始比較皆為相等,則字串長度比較長的較大

console.log('11' > 9);//true,字串和數字比較時,將字串轉為數字比較
console.log('a' > 9);//false,當字串無法轉為數字時,會變為 NaN,不管如何比較,必定回傳 false

console.log(true <= 1);//true,布林和數字比較,true 會轉為 1
console.log(false <= 0);//true, 布林和數字比較,false 會轉為 0

console.log(null == 0);//false,null 如果用等號是套用相等運算符規則,此情況下只等於 undefined,所以等號和大於等於雖然都是比較運算符,但有處理方式不同的情況
console.log(null <= 0);//true,null 和數字比較,總是被轉為 0
console.log(null < 0);//false

console.log(undefined < 0);//false,undefined 和數字進行比較會轉為 NaN,總是回傳 false
console.log(undefined == 0);//false
console.log(undefined > 0);//false

上面概括了原始型別比較時候主要出現的情形,可以發現要避免等式兩邊出現 NaN,否則因為 NaN 的特性,無論和誰比較都會回傳 false,會導致該比較式失效。

至於複合型別,只有兩種處理方式:objectarray

console.log({} > 1);//false
console.log({} <= 1);//false
console.log({} == '[object Object]');//true,object 會被調用 toString() 的方法,預設的 object 的 toString 方法就是回傳這樣,字串和數字比較變為 NaN,故上面兩行皆為 false

console.log([1] >= 0);//true,[1] 被 toString 轉為 1, 1 > 0
console.log([1,2] >= 0);//false,[1,2] 被 toString 轉為 "1,2",字串數字比較無法轉為數字,故左邊變成 NaN,無論怎麼比較都是 false
console.log([1,2] < 0);//false

複合型別在比較的時候做的是稱作抽象運算(Abstract Operation)的過程,會嘗試透過 toPrimitive 轉換來把複合型別變為原始型別,以值表示以進行比較。
詳細的可以去讀文章,簡單的說:

  1. 確認運算子是否有 Hint 方式告知要轉為字串或數字(如 -*/ 會暗示為轉數字,+用於字串連接會暗示轉字串)
  2. 如果有 Hint,數字優先做 valueOf(),字串優先做 toString()
  3. 依照 Hint 的方式轉型,確認是否為原始型別,是的話返回該值使用,否的話調用另一個(如數字接著就調用 toString())
  4. 如果沒有 Hint,會先調用 valueOf(),如果是原始型別,返回該值,否則走 5.
  5. 接著調用 toString(),如果是原始型別,返回該值,否則拋出 TypeError
//例如我們手動複寫 valueOf 和 toString,且都讓他們返回物件本身
const obj = {
  valueOf() {
    return this;
  },
  toString() {
    return this;
  }
};
console.log(obj > 10); // TypeError: Cannot convert object to primitive value
//這樣就會遭遇錯誤,因為無法轉為原始型別  

邏輯運算子和短路機制(Short-circuiting)

比較廣為人知的邏輯運算子就是 &&||!
在進行邏輯運算的時候,往往左右兩邊都不會直接是布林值,這個就涉及到 truthy 和 fasly 的概念,我們在 Day5布林值的轉換 段落提過。

值得一提的是,邏輯運算子聽起來是非 truefalse 的,實際上並不是這樣。
每個運算子有自己的規則,來決定回傳的值。
但因為平常我們通常會接著 if 使用,發生一個隱含轉換成布林值,所以可能平常並不會注意到,但就要小心對 fasly 進行轉換的情況。

這邊我們先提一下關於短路機制(Short-circuiting)。
邏輯運算子裡的 &&||???. 都有這個特性。(???. 後面會提)
如其名,像電路短路的意思是某個地方如果發生了錯誤,就沒辦法繼續往下走,邏輯判斷的短路機制就是在說如果結果已經確定的情況下,那判斷便不會繼續下去。

這會帶來什麼好處?我們可以善加利用這個短路機制,有意的調整擺放順序,讓前面的運算式為我們起到保護作用。

let a = {
  foo:'123'
};
let a = {
  foo:'123'
};
console.log(a.bar && a.bar.length);//undefined

&& 是 AND 運算子,回傳規則是:

  1. 左邊為 fasly,回傳左邊的值,且不處理右邊
  2. 左邊為 truthy,回傳右邊的值
    所以像上面的例子,如果左邊是 undefined,是個 fasly,就直接回傳了左邊,而沒有運行右邊,所以不會導致錯誤的拋出。

而且這邊可以看出 && 回傳的是依 truthy / fasly 的運算元,而不是 true / false 的布林值。

|| 是 OR 運算子,回傳規則是:

  1. 左邊為 truthy,回傳左邊的值,且不處理右邊
  2. 左邊為 fasly,回傳右邊的值

! 則是強轉為布林值,一樣是 Day5 討論轉型有提過關於 !! 的用法了。

空值合併運算子 ??(ES 2021)

?? 稱作空值合併運算子(Nullish)用於判斷左方運算元是否為 null,若為 null 則返回 ?? 右側的值,否則返回左邊的值。

let a = null, b = 10, c, d;
c = a ?? 5;
d = b ?? 5;
console.log(c);//5
console.log(d);//10

使用上十分簡單,算比較新的語法,稍微帶過介紹。
需要注意的只有 ?? 無法和其他邏輯運算子一起連續使用,若需要一起使用,需使用括號讓其他邏輯運算子先結算完返回結果,才能讓該結果與 ?? 做運算。

console.log(true || false ?? 10); //Uncaught SyntaxError: Unexpected token '??'
console.log((true || false) ?? 10); //true

可選串連運算子(?.)(ES 2021)

都提了 ?? 就一起提一下可選串連運算子,同為 ES 2021 推出的運算子,處理的情境也和 ?? 類似。
一般我們會用 . 來進行屬性的訪問,方法的串連等等,但當我們對 nullundefined 使用 . 進行訪問則會拋出 TypeError
?. 就是為此誕生的,如果使用 ?.,則會在訪問對象為 nullundefined 的時候,返回 undefined,見下方例子。

let a = {
  foo : "123",
}
console.log(a.foo.length);//3
//console.log(a.bar.length);//Uncaught TypeError: Cannot read properties of undefined (reading 'length')"
console.log(a.bar?.length);//undefined
console.log(a.bar?.());//undefined,呼叫可能不存在的方法的寫法
console.log(a.bar?.['a']);//undefined,這種方式也能使用 ?.

let b = a.bar&& a.bar.length;//舊的寫法,需要先確認 a.bar 存在
let c = a.bar?.length;//有了 ?. 之後可以更簡潔且安全
let d = a.bar?.length.key;//邏輯短路,不會報錯

且使用 ?. 有個好處,是會觸發邏輯運算子的邏輯短路機制,簡單的說,前面只要因 ?. 回傳了 undefined,該運算式便會即刻中止。舊式寫法容易寫成更複雜的語句嵌套,?. 改善了這點。


沒想到運算子在篇幅上花的其實挺多,剩餘的類別我們會在下篇繼續。


上一篇
詞法作用域(Lexical Scope)與閉包(Closure)
下一篇
運算子(Operator) 下篇(含JS 中的運算子優先級/序)
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言